<?php
/**
 * 
 * Generic loader with static include dirs.
 * Supports Crossley convention class loading with namespaces
 * 
 * @author Alton Crossley <crossleyframework@nogahidebootstrap.com>
 * @package Crossley Framework
 *  
 * @copyright Copyright (c) 2003-2009, Nogaide BootStrap INC. All rights reserved.
 * @license BSD http://opensource.org/licenses/bsd-license.php
 * @version $Id:$
 * 
 */
class X_Loader
{
    /**
     * array of search dirs
     *
     * @var array
     */
    protected static $_aIncludeDirs = array();
    
    /**
     * store trace
     *
     * @var bool
     */
    public static $bTrace = false;
    /**
     * list of attempted loaded files
     *
     * @var array
     */
    public static $aTrace = array();
    

    /**
     * @return array
     */
    public static function getIncludeDirs()
    {
        self::$_aIncludeDirs;
    }
    
    /**
     * @param array $_aIncludeDirs
     */
    public static function setIncludeDirs(Array $aIncludeDirs)
    {
        self::$_aIncludeDirs = $aIncludeDirs;
    }
    /**
     * Loads a class from a PHP file.  The filename must be formatted
     * as "$sClass.php".
     *
     * If $aDirs is a string or an array, it will search the directories
     * in the order supplied, and attempt to load the first matching file.
     *
     * If $aDirs is null, it will split the class name at underscores to
     * generate a path hierarchy (e.g., "X_Example_Class" will map
     * to "X/Example/Class.php").
     *
     * If the file was not found in the $aDirs, or if no $aDirs were specified,
     * it will attempt to load it from PHP's include_path.
     *
     * @param string $sClass      - The full class name
     * @param string|array $aDirs - OPTIONAL Either a path or an array of paths
     *                             to search.
     * @return void
     * @throws X_Exception
     */
    public static function loadClass($sClass, $aDirs = null)
    {
        if (class_exists($sClass, false) || interface_exists($sClass, false))
        {
            return;
        }
        
        if ((null !== $aDirs) && ! is_string($aDirs) && ! is_array($aDirs))
        {
            require_once 'ds/Exception.php';
            throw new X_Exception('Directory argument must be a string or an array');
        }
        
        if (null === $aDirs)
        {
            $aDirs = array();
        }
        if (is_string($aDirs))
        {
            $aDirs = (array) $aDirs;
        }
        
        // autodiscover the path from the class name
        $sPath = str_replace('_', DIRECTORY_SEPARATOR, $sClass);
        
//        if ($sPath != $sClass)
//        {
            $sFile = $sPath . '.php';
            // try the regular search path
            if (self::isReadable($sFile))
            {
                if (self::$bTrace) array_push(self::$aTrace, $sFile . " - IS readable - " . __METHOD__ . '() line:' . __LINE__);
                
                return self::_includeFile($sFile, true);
            }
           
            try
            {
                // try the full search path
                self::_includeFile($sFile);
            }
            catch (Exception $ex)
            {
                array_push(self::$aTrace, $sFile . ' ' . $ex->getMessage());
            }
//        }
        
        if (! class_exists($sClass, false) && ! interface_exists($sClass, false))
        {
            $sClassFile = $sClass . '.php';
            if (self::$bTrace) array_push(self::$aTrace, $sClassFile . " - - " . __METHOD__ . '() line:' . __LINE__);
            self::loadFile($sClassFile, $aDirs, true);
        }
        
        if (! class_exists($sClass, false) && ! interface_exists($sClass, false))
        {
            require_once 'ds/Exception.php';
            throw new X_Exception("File \"$sFile\" was loaded but class \"$sClass\" was not found in the file");
        }
    }
    
    /**
     * spl_autoload() suitable implementation for supporting class autoloading.
     * supports searching a array of include dirs stored statically in
     * $_aIncludeDirs 
     *
     * Attach to spl_autoload() using the following:
     * <code>
     * spl_autoload_register(array('X_Loader', 'autoload'));
     * </code>
     *
     * @param string $class
     * @return string|false Class name on success; false on failure
     */
    public static function autoload($sClassName)
    {
        try
        {
            self::loadClass($sClassName, self::$_aIncludeDirs);
            return $sClassName;
        }
        catch (Exception $e)
        {
            return false;
        }
    }
    /**
     * add to the include path static
     * $bModifyGlobal indicates weather or not to modify the
     * include path in memory.  Set to false if you do not want
     * to reslove underscors in this directory
     *
     * @param string $sPath
     * @param bool $bModifyGlobal
     */
    public static function addIncludePath($sPath, $bModifyGlobal = true)
    {
        $sFilteredPath = realpath((string) $sPath);
        // only add the path if it is valid
        if ($sFilteredPath)
        {
            self::_initIncludePath();
            $iIndex = array_unshift(self::$_aIncludeDirs, $sFilteredPath);
            //X_Debug::out($sFilteredPath, "added to path at index {$iIndex}");
        }
        else
        {
            throw new X_Exception("invalid path {$sPath}");
        }
        
        if ($bModifyGlobal)
        {
            set_include_path(implode(PATH_SEPARATOR, self::$_aIncludeDirs));
        }
        
        return $sFilteredPath;
    }
    
    /**
     * add current search path to $_aIncludeDirs
     *
     */
    protected static function _initIncludePath()
    {
        if (count(self::$_aIncludeDirs))
        {
            // already inited
            return;
        }
        else
        {
            self::$_aIncludeDirs = explode(PATH_SEPARATOR, get_include_path());
            //X_Debug::out(self::$_aIncludeDirs, 'breaking up include path');
        }
    }
    
    /**
     * Register {@link autoload()} with spl_autoload()
     * defaults to X_Loader
     * 
     * adapted from Zend_Loader
     *
     * @param string OPTIONAL $class
     * @param boolean OPTIONAL $enabled
     * @return void
     * @throws X_Exception if spl_autoload() is not found
     * or if the specified class does not have an autoload() method.
     */
    public static function registerAutoload($class = 'X_Loader', $enabled = true)
    {
        if (! function_exists('spl_autoload_register'))
        {
            require_once 'X/Exception.php';
            throw new X_User_Exception_Exception('spl_autoload does not exist in this PHP installation');
        }
        
        self::loadClass($class);
        $methods = get_class_methods($class);
        if (! in_array('autoload', (array) $methods))
        {
            require_once 'X/Exception.php';
            throw new X_Exception("The class \"$class\" does not have an autoload() method");
        }
        
        if ($enabled === true)
        {
            spl_autoload_register(array($class , 'autoload'));
        }
        else
        {
            spl_autoload_unregister(array($class , 'autoload'));
        }
    }
    
    /**
     * Redefine isReadable() to nuke annoying debug warnings
     * thrown by parent::isReadable() (@fopen)
     * NOTE: the general order of checking here is
     * - current working directory with class name as-is (including underscores)
     * - other include paths (in order) with class name as-is
     * - cwd + class name with underscores -> directory separators
     * - other include paths with the same
     * (see also Alton's docblock on loadClass() above)
     *
     * @see http://framework.zend.com/issues/browse/ZF-3249
     * @param string $filename
     * @return boolean
     */
    public static function isReadable($filename)
    {
        static $aIncludePaths = null;
        if ($aIncludePaths == null)
        {
            $aIncludePaths = explode(PATH_SEPARATOR, get_include_path());
        }
        
        foreach ($aIncludePaths as $sIncludePath)
        {
            $sFullpath = $sIncludePath . DIRECTORY_SEPARATOR . $filename;
            if (@file_exists($sFullpath) && @is_readable($sFullpath))
            {
                return true;
            }
            else 
            {
//            	if (self::$bTrace) array_push(self::$aTrace, $sFullpath . " - not readable - " . __METHOD__ . '() line:' . __LINE__);
            }
        }
        
        return (@file_exists($filename) && @is_readable($filename));
    }
    
    /**
     * Loads a PHP file.  This is a wrapper for PHP's include() function.
     *
     * $filename must be the complete filename, including any
     * extension such as ".php".  Note that a security check is performed that
     * does not permit extended characters in the filename.  This method is
     * intended for loading Zend Framework files.
     *
     * If $dirs is a string or an array, it will search the directories
     * in the order supplied, and attempt to load the first matching file.
     *
     * If the file was not found in the $dirs, or if no $dirs were specified,
     * it will attempt to load it from PHP's include_path.
     *
     * If $once is TRUE, it will use include_once() instead of include().
     *
     * @param  string        $filename
     * @param  string|array  $dirs - OPTIONAL either a path or array of paths
     *                       to search.
     * @param  boolean       $once
     * @return boolean
     * @throws X_Exception
     */
    public static function loadFile($sFileName, $xDirs = null, $bOnce = false)
    {
        /**
         * Security check
         */
        if (preg_match('/[^a-z0-9\-_.]/i', $sFileName))
        {
            require_once 'X/Exception.php';
            throw new X_Exception('Security check: Illegal character in filename');
        }
        
        /**
         * Search for the file in each of the dirs named in $dirs.
         */
        if (is_null($xDirs))
        {
            $xDirs = array();
        }
        else if (is_string($xDirs))
        {
            $xDirs = explode(PATH_SEPARATOR, $xDirs);
        }
        foreach ($xDirs as $dir)
        {
            $sFilespec = rtrim($dir, '\\/') . DIRECTORY_SEPARATOR . $sFileName;
            if (self::isReadable($sFilespec))
            {
                return self::_includeFile($sFilespec, $bOnce);
            }
        }
        
        /**
         * The file was not found in the $dirs specified.
         * Try finding for the plain filename in the include_path.
         */
        if (self::isReadable($sFileName))
        {
        	if (self::$bTrace) array_push(self::$aTrace, $sFileName . " - FILE READABLE - " . __METHOD__ . '() line:' . __LINE__);
            return self::_includeFile($sFileName, $bOnce);
        }
        
        /**
         * The file was not located anywhere.
         */
        require_once 'X/Exception.php';
        if (self::$bTrace) array_push(self::$aTrace, $sFileName . " - not found - " . __METHOD__ . '() line:' . __LINE__);
        throw new X_Exception("File \"$sFileName\" was not found");
    }
    
    /**
     * Attempt to include() the file.
     *
     * include() is not prefixed with the @ operator because if
     * the file is loaded and contains a parse error, execution
     * will halt silently and this is difficult to debug.
     *
     * Always set display_errors = Off on production servers!
     *
     * @param  string  $filespec
     * @param  boolean $once
     * @return boolean
     */
    protected static function _includeFile($sFilespec, $bOnce = true)
    {
        /**
         * @hack
         */
        set_error_handler("exception_error_handler");
        try
        {
            if ($bOnce)
            {
                if (self::$bTrace) array_push(self::$aTrace, $sFilespec . " - INCLUDE_ONCE - " . __METHOD__ . '() line:' . __LINE__);
                include_once $sFilespec;  $iLine = __LINE__;
            }
            else
            {
                if (self::$bTrace) array_push(self::$aTrace, $sFilespec . " - INCLUDE - " . __METHOD__ . '() line:' . __LINE__);
                include $sFilespec; $iLine = __LINE__;
            }
        }
        catch (Exception $e)
        {
            if ($e->getFile() == __FILE__ && $e->getLine() == $iLine)
            {
                return false;
            }
            else 
            {
                throw $e;
            }
        }
        restore_error_handler();
        return true;
    }
    
}

function exception_error_handler($errno, $errstr, $errfile, $errline ) {
            throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
    }